/**
 * Copyright (c) 2010-2011 Pierre LABATUT
 *
 * This file is part of multiple-track-player project.
 *
 * multiple-track-player is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/multiple-track-player/>.
 */
package multitrack.preload;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFormat.Encoding;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import multitrack.annotation.NotNull;

/**
 * Produce PCM data from a source, and push them to a buffer.
 * This class implements Runnable, to read AudioInputStream by chunk, convert bytes to
 * shorts and append the data to a buffer. When the end is reached the AudioInputStream
 * is closed, and the buffer is marked as complete.
 * 
 * @author Pierre LABATUT
 */
public class AudioProducer implements Runnable {

    // Where the input Signed PCM data is read as bytes.
    @NotNull
    private final AudioInputStream ais;
    
    // Where the short are output.
    @NotNull
    private final HeapAudioBuffer buffer;
    
    // Size of the chunk readed and written, in sample (aka in short)
    // This must be a multiple of the channel count in order to avoid frame splitting
    private final int chunkSizeInSample;

    public AudioProducer(final URL url, final int chunkSizeInFrame) throws IOException, UnsupportedAudioFileException {
        if (url == null) {
            throw new IllegalArgumentException("Need an URL to produce audio data");
        }
        if (chunkSizeInFrame <= 0) {
            throw new RuntimeException("Negative chunk size not allowed");
        }
        
        /**
         * Get an audio input stream to this file. Providing an input stream avoids 
         * the url to be resolved several times, while trying to find an appropriate
         * decoder.
         */
        final InputStream openStream = url.openStream();
        final AudioInputStream tais = AudioSystem.getAudioInputStream(openStream);
        /**
         * The stream may no be Signed PCM 16. The frequency and channel number is free.
         * Lets find a converter.
         */
        if (tais.getFormat().getEncoding().equals(Encoding.PCM_SIGNED) == false) {
            //frame rate = sample rate for PCM. 
            final AudioFormat targetFormat = new AudioFormat(Encoding.PCM_SIGNED,
                    tais.getFormat().getSampleRate(), 16, tais.getFormat().getChannels(),
                    2 * 2, tais.getFormat().getSampleRate(), tais.getFormat().isBigEndian());
            ais = AudioSystem.getAudioInputStream(targetFormat, tais);
        } else {
            ais = tais;
        }
        
        final AudioFormat format = ais.getFormat();
        if (format.getSampleSizeInBits() != Short.SIZE) {
            throw new IllegalArgumentException("Sample must be 16 bit");
        }
        final int frameSizeInByte = format.getFrameSize();
        if (frameSizeInByte == AudioSystem.NOT_SPECIFIED) {
            throw new IllegalArgumentException("Undefined frame size not allowed");
        }

        if (frameSizeInByte <= 0) {
            throw new RuntimeException("Negative frame size not allowed");
        }
        if (frameSizeInByte != format.getChannels() * 2) {
            throw new RuntimeException("Stride should be null");
        }
        chunkSizeInSample = chunkSizeInFrame * frameSizeInByte;

        buffer = new HeapAudioBuffer(format);
    }

    public HeapAudioBuffer buffer() {
        return buffer;
    }

    @Override
    public void run() {

        try {
            // where byte data is read from the AudioInputStream
            final byte byteChunk[] = new byte[chunkSizeInSample * 2];
            // where the byte data is converted into short 
            final short shortChunk[] = new short[chunkSizeInSample];
            // where the short data is output
            final ShortWriter output = buffer.getWriter();
            try {
                try {
                    while (true) {
                        // get a chunk or a partial chunk made of complete frames (see api)
                        final int lenInByte = ais.read(byteChunk);
                        if (lenInByte == -1) {
                            //end of stream reached
                            break;
                        }
                        Bits.byteToShort(byteChunk, 0, lenInByte, shortChunk, 0, ais.getFormat().isBigEndian());
                        output.write(shortChunk, 0, lenInByte / 2);
                    }


                } finally {
                    output.close();
                }
            } finally {
                ais.close();
            }

        } catch (IOException ex) {
            //There is nothing I can do there
            //TODO: Handle this error case so that a nice error message is displayed
            throw new RuntimeException(ex);
        }
    }
}
